Skip to main content

Svelte 5 State Management

SvelteBolt provides a powerful and modern approach to state management using TypeScript classes combined with Svelte's context API and Svelte 5's reactivity system.

Overview​

The state management pattern in SvelteBolt leverages:

  • TypeScript classes for organizing state logic
  • Svelte's context API for dependency injection
  • Svelte 5's $state runes for reactivity in .svelte.ts files
  • Clean separation between different state concerns

This approach eliminates the complexity of traditional stores while providing powerful reactivity and type safety.

Basic App State​

Setting Up App State​

The main application state is defined in a TypeScript class:

import { getContext, setContext } from "svelte";

class AppState {
// Reactive state using Svelte 5's $state rune
user = $state(null);
theme = $state("light");
isLoading = $state(false);

constructor() {
// Initialize state or set up derived values
}

// Methods to update state
setUser(user) {
this.user = user;
}

toggleTheme() {
this.theme = this.theme === "light" ? "dark" : "light";
}

setLoading(loading) {
this.isLoading = loading;
}
}

export function setAppState() {
return setContext("appState", new AppState());
}

export function getAppState() {
return getContext<ReturnType<typeof setAppState>>("appState");
}

Using App State​

In Parent Component (Setting Context)​

In your root layout or parent component, initialize the state:

<script lang="ts">
import { setAppState } from "./app-state.svelte.ts";

// Initialize the app state context
setAppState();
</script>

<!-- Your app content -->
<slot />

In Child Components (Consuming State)​

Any child component can access and use the state:

<script lang="ts">
import { getAppState } from "../app-state.svelte.ts";

const appState = getAppState();

function handleLogin() {
appState.setUser({ name: "John Doe", email: "[email protected]" });
}

function toggleTheme() {
appState.toggleTheme();
}
</script>

<!-- Reactive to state changes -->
<div class="theme-{appState.theme}">
{#if appState.user}
<p>Welcome, {appState.user.name}!</p>
{:else}
<button onclick="{handleLogin}">Login</button>
{/if}

<button onclick="{toggleTheme}">
Switch to {appState.theme === 'light' ? 'dark' : 'light'} mode
</button>
</div>

Key Benefits​

1. Svelte 5 Reactivity in TypeScript​

Using .svelte.ts file extension enables Svelte 5's reactivity system in TypeScript files:

class AppState {
// These are reactive and will trigger UI updates
count = $state(0);
items = $state([]);

// Derived values are also reactive
get doubleCount() {
return this.count * 2;
}

increment() {
this.count++; // Automatically triggers reactivity
}
}

2. Type Safety​

Full TypeScript support with proper typing:

interface User {
id: string;
name: string;
email: string;
}

class AppState {
user = $state<User | null>(null);

setUser(user: User) {
this.user = user;
}
}

3. No Store Complexity​

Unlike traditional Svelte stores, this pattern:

  • ✅ No need to import/export stores
  • ✅ No subscription management
  • ✅ No store boilerplate
  • ✅ Natural OOP patterns
  • ✅ Better IDE support

Creating Specific States​

For different concerns, create separate state classes:

Editor State Example​

// editor-state.svelte.ts
import { getContext, setContext } from "svelte";

class EditorState {
content = $state("");
cursor = $state({ line: 0, column: 0 });
isModified = $state(false);
language = $state("javascript");

constructor() {}

updateContent(newContent: string) {
this.content = newContent;
this.isModified = true;
}

setCursor(line: number, column: number) {
this.cursor = { line, column };
}

save() {
// Save logic here
this.isModified = false;
}
}

export function setEditorState() {
return setContext("editorState", new EditorState());
}

export function getEditorState() {
return getContext<ReturnType<typeof setEditorState>>("editorState");
}

Shopping Cart State Example​

// cart-state.svelte.ts
import { getContext, setContext } from "svelte";

interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}

class CartState {
items = $state<CartItem[]>([]);

get total() {
return this.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}

get itemCount() {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}

addItem(item: Omit<CartItem, "quantity">) {
const existing = this.items.find((i) => i.id === item.id);
if (existing) {
existing.quantity++;
} else {
this.items.push({ ...item, quantity: 1 });
}
}

removeItem(id: string) {
this.items = this.items.filter((item) => item.id !== id);
}

clear() {
this.items = [];
}
}

export function setCartState() {
return setContext("cartState", new CartState());
}

export function getCartState() {
return getContext<ReturnType<typeof setCartState>>("cartState");
}

Best Practices​

1. File Naming Convention​

  • Use .svelte.ts extension for reactive TypeScript files
  • Name state files descriptively: app-state.svelte.ts, editor-state.svelte.ts

2. Context Keys​

  • Use descriptive context keys that match your state purpose
  • Keep context scope as narrow as possible

3. State Organization​

  • Separate concerns into different state classes
  • Keep related state and methods together in the same class
  • Use getter methods for derived values

4. Initialization​

  • Initialize state in the appropriate parent component
  • Ensure state is available before child components try to access it

Migration from Stores​

If you're migrating from traditional Svelte stores:

// Old store approach
import { writable } from "svelte/store";
export const user = writable(null);
export const theme = writable("light");

// New class-based approach
class AppState {
user = $state(null);
theme = $state("light");

setUser(newUser) {
this.user = newUser;
}

toggleTheme() {
this.theme = this.theme === "light" ? "dark" : "light";
}
}

The class-based approach provides better organization, type safety, and eliminates the need for store subscriptions while maintaining full reactivity.